using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Text;
using Beaker.Specs;

namespace OpenCube.Nbt.Specs.Builders
{
	[CLSCompliant(false)]
	public sealed class NbtStreamBuilder
		: Builder<Stream>
	{
		private readonly MemoryStream _stream = new MemoryStream();
		private readonly BinaryWriter _writer;

		public NbtStreamBuilder()
		{
			_writer = new BinaryWriter(_stream);
		}

		private NbtStreamBuilder WriteTag(NbtTag tag)
		{
			_writer.Write((byte)tag);
			return this;
		}

		private NbtStreamBuilder WriteString(string name)
		{
			Contract.Requires<ArgumentNullException>(!String.IsNullOrEmpty(name));
			Contract.Requires<ArgumentOutOfRangeException>(name.Length <= 32767, "NBT Strings cannot be longer as 32767 chars");

			byte[] data = Encoding.UTF8.GetBytes(name);

			// big endian write of length
			WriteInt16((short)data.Length);

			// write utf8 string
			_writer.Write(data, 0, data.Length);
			return this;
		}

		public NbtStreamBuilder AppendCompound(string compoundName)
		{
			WriteTag(NbtTag.Compound);
			WriteString(compoundName);

			var builder = new NbtStreamBuilder();
			builder._endCompound =
				() =>
				{
					byte[] content = builder._stream.ToArray();
					WriteBytes(content);
					WriteTag(NbtTag.End);
					return this;
				};

			return builder;
		}

		private Func<NbtStreamBuilder> _endCompound;

		public NbtStreamBuilder EndCompound()
		{
			return _endCompound();
		}

		public NbtStreamBuilder AppendString(string name, string value)
		{
			Contract.Requires<ArgumentNullException>(!String.IsNullOrEmpty(name));
			Contract.Requires<ArgumentOutOfRangeException>(name.Length <= 32767, "NBT Strings cannot be longer as 32767 chars");
			Contract.Requires<ArgumentNullException>(!String.IsNullOrEmpty(value));
			Contract.Requires<ArgumentOutOfRangeException>(value.Length <= 32767, "NBT Strings cannot be longer as 32767 chars");

			WriteTag(NbtTag.String);
			WriteString(name);
			WriteString(value);

			return this;
		}

		public NbtStreamBuilder AppendSByte(string name, sbyte value)
		{
			Contract.Requires<ArgumentNullException>(!String.IsNullOrEmpty(name));
			Contract.Requires<ArgumentOutOfRangeException>(name.Length <= 32767, "NBT Strings cannot be longer as 32767 chars");

			WriteTag(NbtTag.Byte);
			WriteString(name);
			WriteSByte(value);

			return this;
		}

		public NbtStreamBuilder AppendInt16(string name, short value)
		{
			Contract.Requires<ArgumentNullException>(!String.IsNullOrEmpty(name));
			Contract.Requires<ArgumentOutOfRangeException>(name.Length <= 32767, "NBT Strings cannot be longer as 32767 chars");

			WriteTag(NbtTag.Short);
			WriteString(name);
			WriteInt16(value);

			return this;
		}

		public NbtStreamBuilder AppendInt32(string name, int value)
		{
			Contract.Requires<ArgumentNullException>(!String.IsNullOrEmpty(name));
			Contract.Requires<ArgumentOutOfRangeException>(name.Length <= 32767, "NBT Strings cannot be longer as 32767 chars");

			WriteTag(NbtTag.Int);
			WriteString(name);
			WriteInt32(value);

			return this;
		}

		public NbtStreamBuilder AppendInt64(string name, long value)
		{
			Contract.Requires<ArgumentNullException>(!String.IsNullOrEmpty(name));
			Contract.Requires<ArgumentOutOfRangeException>(name.Length <= 32767, "NBT Strings cannot be longer as 32767 chars");

			WriteTag(NbtTag.Long);
			WriteString(name);
			WriteInt64(value);

			return this;
		}

		public NbtStreamBuilder AppendListOfSByte(string listName, params sbyte[] values)
		{
			return AppendList(listName, NbtTag.Byte, values.ToList(), i => WriteSByte(i));
		}

		public NbtStreamBuilder AppendListOfInt16(string listName, params short[] values)
		{
			return AppendList(listName, NbtTag.Short, values.ToList(), i => WriteInt16(i));
		}

		public NbtStreamBuilder AppendListOfInt32(string listName, params int[] values)
		{
			return AppendList(listName, NbtTag.Int, values.ToList(), i => WriteInt32(i));
		}

		public NbtStreamBuilder AppendListOfInt64(string listName, params long[] values)
		{
			return AppendList(listName, NbtTag.Long, values.ToList(), i => WriteInt64(i));
		}

		public NbtStreamBuilder AppendListOfSingle(string listName, params float[] values)
		{
			return AppendList(listName, NbtTag.Float, values.ToList(), i => WriteSingle(i));
		}

		private NbtStreamBuilder AppendList<T>(string name, NbtTag itemTag, IList<T> items, Action<T> write)
		{
			WriteTag(NbtTag.List);
			WriteString(name);
			WriteTag(itemTag);
			WriteInt32(items.Count);
			foreach (T item in items)
				write(item);

			return this;
		}

		#region InternalWriters

		private NbtStreamBuilder WriteByte(byte value)
		{
			_writer.Write(value);
			return this;
		}

		private NbtStreamBuilder WriteSByte(sbyte value)
		{
			_writer.Write(value);
			return this;
		}

		private NbtStreamBuilder WriteInt16(short value)
		{
			byte[] data = BitConverter.GetBytes(value);

			// reverse if system is little endian, because big endian is expected for Nbt format
			if (BitConverter.IsLittleEndian)
				Array.Reverse(data);

			_writer.Write(data, 0, 2);

			return this;
		}

		private NbtStreamBuilder WriteInt32(int value)
		{
			byte[] data = BitConverter.GetBytes(value);

			// reverse if system is little endian, because big endian is expected for Nbt format
			if (BitConverter.IsLittleEndian)
				Array.Reverse(data);

			_writer.Write(data, 0, 4);

			return this;
		}

		private NbtStreamBuilder WriteInt64(long value)
		{
			byte[] data = BitConverter.GetBytes(value);

			// reverse if system is little endian, because big endian is expected for Nbt format
			if (BitConverter.IsLittleEndian)
				Array.Reverse(data);

			_writer.Write(data, 0, 8);

			return this;
		}

		private NbtStreamBuilder WriteSingle(float value)
		{
			byte[] data = BitConverter.GetBytes(value);

			// reverse if system is little endian, because big endian is expected for Nbt format
			if (BitConverter.IsLittleEndian)
				Array.Reverse(data);

			_writer.Write(data, 0, 4);

			return this;
		}

		private NbtStreamBuilder WriteDouble(double value)
		{
			byte[] data = BitConverter.GetBytes(value);

			// reverse if system is little endian, because big endian is expected for Nbt format
			if (BitConverter.IsLittleEndian)
				Array.Reverse(data);

			_writer.Write(data, 0, 8);

			return this;
		}

		private NbtStreamBuilder WriteBytes(byte[] value)
		{
			_writer.Write(value);
			return this;
		}

		private NbtStreamBuilder WriteSBytes(sbyte[] value)
		{
			// convert sbyte to byte
			var buffer = new byte[value.Length];
			Buffer.BlockCopy(value, 0, buffer, 0, value.Length);

			// write
			_writer.Write(buffer);
			return this;
		}

		#endregion

		public override Stream Build()
		{
			if (_endCompound != null)
				_endCompound();

			return new MemoryStream(_stream.ToArray());
		}
	}
}